Design - Command Framework

Wednesday, March 22, 2023

10:34 AM

The OneMore add-in extends OneNote with many new commands. Just like most OneNote commands, the OneMore commands are available through a set of concise menus on the Home ribbon bar. They're also available through a number of other mechanisms. Commands generally have the following requirements.

 

Requirements

  • Commands should be created and destroyed using a standard pattern
  • Commands should be accessible from the ribbon, from hotkeys, and from the Command Palette
  • Commands should be discoverable such that they can be enumerated and added to context menus
  • Commands should have access to the Ribbon to update or add/remove controls, e.g. Favorites or Snippets
  • Commands should be tracked through a most-recently-used list, MRU
  • Commands should be fully describable such that the MRU can hand that to Replay and Palette functions

 

Command Use Cases

 

Ultimately, the goal is to simplify how we add new commands and automate all of these use cases. In How to Add a New Command, it shows that this can be done in three easy steps:

 

  1. Implement the command, inheriting from the Command base class
  2. Implement the proxy method in the AddInCommands.cs file
  3. Add a new button control in the ribbon by declaring it in the Ribbon.xml file

 

Assumptions

  • Controls in the ribbon.xml include the onAction attribute, specifying the name of a callback method. Every callback method must be an imperative member, defined in the AddIn scope
  • Everything must run async and be thread-safe
  • Each command invocation should be as isolated as possible and behave like a good neighbor

 

Models

Obviously, the AddIn class is the root of everything. It implements IDTExtensibility2 that declares add-in lifecycle handlers from which we can grab a reference to IRibbonControl and IRibbonExtensibility,  the latter of which declares the GetCustomUI callback where our responsibility is to return XML describing the add-in extensions to the ribbon.

 

We leverage the IDTEExtensibility2.OnStartupComplete handler to instantiate our own CommandFactory and start up any internal services such as the Command Service, Reminder Service[LINK] and Navigation Service[LINK].

 

The AddIn class is divided into multiple C# files, one of which is AddInCommands.cs. This file contains callback methods, each bound to a ribbon control via its onAction property. By convention, each callback method is a very simple proxy that uses the CommandFactory to instantiate and invoke a single command.

 

Additionally, each callback method may be decorated with a [CommandAttribute] that declares the resource ID used to lookup the translation string for the command, key definitions that declare the default shortcut key sequence, and an optional category for grouping commands in the Settings dialog.

 

[Command("ribAddFootnoteButton_Label", Keys.Control | Keys.Alt | Keys.F, "ribReferencesMenu")]

public async Task AddFootnoteCmd(IRibbonControl control)

=> await factory.Run<AddFootnoteCommand>();

 

Command Class

All commands derive from the abstract base class Command. Created by CommandFactory, each instance inherits protected fields to access an ILogger, the IRibbonUI, and a reference to the factory, allowing commands to invoke other commands.

 

The abstract Execute method must be implemented by inheritors and encapsulates the business logic of the command.

 

Command

 

 

CommandFactory Class

This factory class is responsible for instantiating new commands and initializing their context including ILogger and IRibbonUI references.

 

The Run method dispatches the command to a new thread, calls its Execute method, and if successful and not cancelled, records it in the MRU.

 

CommandFactory

 

 

 

──────────────────────────────────────────────────────────────────────────────────────────────────

Command Use Cases PlantUML (Refresh)

@startuml Command Use Cases

scale max 590 width

skin rose

top to bottom direction

usecase (Command) as cmd #LightSkyBlue

usecase Hotkeys

usecase Replay

usecase Palette

usecase Ribbon

usecase "Context Menus" as menus

usecase Aliases

cmd --> Hotkeys

cmd --> Palette

cmd --> Ribbon

cmd --> Replay

cmd --> menus

cmd --> Aliases

@enduml

 

Command PlantUML (Refresh)

@startuml Command

skinparam defaultFontSize 9

class Command {

 #logger : ILogger

 #ribbon : IRibbonUI

 #factory : CommandFactory

 +get_IsCancelled : bool

 +XElement GetReplayArguments()

 +{abstract} Task Execute(params object[] args)

}

@enduml

 

CommandFactory PlantUML (Refresh)

@startuml CommandFactory

skinparam defaultFontSize 9

class CommandFactory {

 +Task Invoke(string action, string[] arguments)

 +Task ReplayLastAction()

 +Task Run(params object[] args)

}

@enduml

 

 

#omwiki #omdeveloper #omdesign

 

© 2020 Steven M Cohn. All rights reserved.

Please consider a sponsorship or one-time donation to support ongoing development

 

 

Created with OneNote.